
/*-------------------------------------------------------------------------
 *
 * portalcmds.c
 *	  影响门户的实用命令（即，SQL 游标命令）
 *
 * 注意：请参阅 tcop/pquery.c，它实现了 FE/BE 协议的门户操作。该模块使用 pquery.c 进行某些操作。
 * 这两个模块都依赖于 utils/mmgr/portalmem.c，该模块控制门户的存储管理（但不在其中运行任何查询）。
 *
 *
 * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
 * Portions Copyright (c) 1994, Regents of the University of California
 *
 *
 * IDENTIFICATION
 *	  src/backend/commands/portalcmds.c
 *
 *-------------------------------------------------------------------------
 */

#include "postgres.h"

#include <limits.h>

#include "access/xact.h"
#include "commands/portalcmds.h"
#include "executor/executor.h"
#include "executor/tstoreReceiver.h"
#include "miscadmin.h"
#include "rewrite/rewriteHandler.h"
#include "tcop/pquery.h"
#include "tcop/tcopprot.h"
#include "utils/memutils.h"
#include "utils/snapmgr.h"


/*
 * PerformCursorOpen
 * 执行 SQL DECLARE CURSOR 命令。
 */
void PerformCursorOpen(ParseState *fc_pstate, DeclareCursorStmt *fc_cstmt, ParamListInfo fc_params,
				  bool fc_isTopLevel)
{
	Query	   *fc_query = castNode(Query, fc_cstmt->query);
	List	   *fc_rewritten;
	PlannedStmt *fc_plan;
	Portal		fc_portal;
	MemoryContext fc_oldContext;
	char	   *fc_queryString;

	/*
 * 不允许空字符串光标名称（与协议级别的无名门户冲突）。
 */
	if (!fc_cstmt->portalname || fc_cstmt->portalname[0] == '\0')
		ereport(ERROR,
				(errcode(ERRCODE_INVALID_CURSOR_NAME),
				 errmsg("invalid cursor name: must not be empty")));

	/*
 * 如果这是一个不可持有的光标，我们要求该语句在事务块内执行（否则，它将没有用户可见的效果）。
 */
	if (!(fc_cstmt->options & CURSOR_OPT_HOLD))
		RequireTransactionBlock(fc_isTopLevel, "DECLARE CURSOR");
	else if (InSecurityRestrictedOperation())
		ereport(ERROR,
				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
				 errmsg("cannot create a cursor WITH HOLD within security-restricted operation")));

	/*
	 * 解析分析已经完成，但我们仍然需要运行规则
	 * 重写器。我们不执行 AcquireRewriteLocks：我们假设查询要么
	 * 直接来自解析器，或者适当的锁由
	 * plancache.c 获取。
	 */
	fc_rewritten = QueryRewrite(fc_query);

	/* SELECT 不应该重写为多于或少于一个查询 */
	if (list_length(fc_rewritten) != 1)
		elog(ERROR, "non-SELECT statement in DECLARE CURSOR");

	fc_query = linitial_node(Query, fc_rewritten);

	if (fc_query->commandType != CMD_SELECT)
		elog(ERROR, "non-SELECT statement in DECLARE CURSOR");

	/* 计划查询，应用指定的选项 */
	fc_plan = pg_plan_query(fc_query, fc_pstate->p_sourcetext, fc_cstmt->options, fc_params);

	/*
 * 创建一个门户并将计划和查询字符串复制到其内存中。
 */
	fc_portal = CreatePortal(fc_cstmt->portalname, false, false);

	fc_oldContext = MemoryContextSwitchTo(fc_portal->portalContext);

	fc_plan = copyObject(fc_plan);

	fc_queryString = pstrdup(fc_pstate->p_sourcetext);

	PortalDefineQuery(fc_portal,
					  NULL,
					  fc_queryString,
					  CMDTAG_SELECT,	/* 光标的查询始终是 SELECT */
					  list_make1(fc_plan),
					  NULL);

	/*----------
 * 还将外部门户的参数列表复制到内部门户的内存上下文中。我们希望传递参数值，以防我们有这样的命令
 * DECLARE c CURSOR FOR SELECT ... WHERE foo = $1
 * 这将使用外部参数集进行解析，参数值需要保留以便在光标执行时使用。
 *----------
 */
	fc_params = copyParamList(fc_params);

	MemoryContextSwitchTo(fc_oldContext);

	/*
 * 设置门户的选项。
 *
 * 如果用户没有指定 SCROLL 类型，则根据是否需要任何额外的运行时开销来允许或不允许滚动。同时，我们不允许 FOR UPDATE 光标进行滚动。
 */
	fc_portal->cursorOptions = fc_cstmt->options;
	if (!(fc_portal->cursorOptions & (CURSOR_OPT_SCROLL | CURSOR_OPT_NO_SCROLL)))
	{
		if (fc_plan->rowMarks == NIL &&
			ExecSupportsBackwardScan(fc_plan->planTree))
			fc_portal->cursorOptions |= CURSOR_OPT_SCROLL;
		else
			fc_portal->cursorOptions |= CURSOR_OPT_NO_SCROLL;
	}

	/*
 * 开始执行，如果有的话插入参数。
 */
	PortalStart(fc_portal, fc_params, 0, GetActiveSnapshot());

	Assert(fc_portal->strategy == PORTAL_ONE_SELECT);

	/*
 * 我们完成了；查询实际不会运行，直到调用 PerformPortalFetch。
 */
}

/*
 * PerformPortalFetch
 * 执行 SQL FETCH 或 MOVE 命令。
 *
 * stmt: 命令的解析树节点
 * dest: 结果发送到何处
 * qc: 存储命令完成状态数据的位置。
 *
 * 如果调用者不需要状态数据，qc 可以为 NULL。
 */
void PerformPortalFetch(FetchStmt *fc_stmt,
				   DestReceiver *fc_dest,
				   QueryCompletion *fc_qc)
{
	Portal		fc_portal;
	uint64		fc_nprocessed;

	/*
 * 不允许空字符串光标名称（与协议级别的无名门户冲突）。
 */
	if (!fc_stmt->portalname || fc_stmt->portalname[0] == '\0')
		ereport(ERROR,
				(errcode(ERRCODE_INVALID_CURSOR_NAME),
				 errmsg("invalid cursor name: must not be empty")));

	/* 从门户名称获取门户 */
	fc_portal = GetPortalByName(fc_stmt->portalname);
	if (!PortalIsValid(fc_portal))
	{
		ereport(ERROR,
				(errcode(ERRCODE_UNDEFINED_CURSOR),
				 errmsg("cursor \"%s\" does not exist", fc_stmt->portalname)));
		return;					/* 保持编译器开心 */
	}

	/* 如果需要，调整目标。MOVE 需要目标 DestNone */
	if (fc_stmt->ismove)
		fc_dest = None_Receiver;

	/* 执行它 */
	fc_nprocessed = PortalRunFetch(fc_portal,
								fc_stmt->direction,
								fc_stmt->howMany,
								fc_dest);

	/* 如果需要，返回命令状态 */
	if (fc_qc)
		SetQueryCompletion(fc_qc, fc_stmt->ismove ? CMDTAG_MOVE : CMDTAG_FETCH,
						   fc_nprocessed);
}

/*
 * PerformPortalClose
 * 关闭光标。
 */
void PerformPortalClose(const char *fc_name)
{
	Portal		fc_portal;

	/* NULL 表示 CLOSE ALL */
	if (fc_name == NULL)
	{
		PortalHashTableDeleteAll();
		return;
	}

	/*
 * 不允许空字符串光标名称（与协议级别的无名门户冲突）。
 */
	if (fc_name[0] == '\0')
		ereport(ERROR,
				(errcode(ERRCODE_INVALID_CURSOR_NAME),
				 errmsg("invalid cursor name: must not be empty")));

	/*
 * 从门户名称获取门户
 */
	fc_portal = GetPortalByName(fc_name);
	if (!PortalIsValid(fc_portal))
	{
		ereport(ERROR,
				(errcode(ERRCODE_UNDEFINED_CURSOR),
				 errmsg("cursor \"%s\" does not exist", fc_name)));
		return;					/* 保持编译器开心 */
	}

	/*
 * 注意：如果尚未完成，将作为副作用调用 PortalCleanup。
 */
	PortalDrop(fc_portal, false);
}

/*
 * PortalCleanup
 *
 * 删除门户时清理门户。这是门户的标准清理钩子。
 *
 * 注意：如果 portal->status 为 PORTAL_FAILED，我们可能在错误中止期间被调用，必须小心避免执行任何可能再次失败的操作。
 */
void PortalCleanup(Portal fc_portal)
{
	QueryDesc  *fc_queryDesc;

	/*
	 * 健康检查
	 */
	AssertArg(PortalIsValid(fc_portal));
	AssertArg(fc_portal->cleanup == PortalCleanup);

	/*
	 * 关闭执行器，如果仍在运行。我们在错误中止时跳过此操作，
	 * 因为其他机制将负责释放执行器资源，
	 * 而且我们不能确保ExecutorEnd本身不会失败。
	 */
	fc_queryDesc = fc_portal->queryDesc;
	if (fc_queryDesc)
	{
		/*
		 * 在其他任何操作之前重置查询描述符。这样可以防止我们在下面的错误中
		 * 尝试关闭执行器两次。事务中止机制将在这种情况下负责资源清理。
		 */
		fc_portal->queryDesc = NULL;

		if (fc_portal->status != PORTAL_FAILED)
		{
			ResourceOwner fc_saveResourceOwner;

			/* 我们必须使门户的资源所有者为当前状态 */
			fc_saveResourceOwner = CurrentResourceOwner;
			if (fc_portal->resowner)
				CurrentResourceOwner = fc_portal->resowner;

			ExecutorFinish(fc_queryDesc);
			ExecutorEnd(fc_queryDesc);
			FreeQueryDesc(fc_queryDesc);

			CurrentResourceOwner = fc_saveResourceOwner;
		}
	}
}

/*
 * PersistHoldablePortal
 *
 * 准备指定的门户，以便在当前事务之外访问。该函数返回时，所有未来对
 * 门户的访问必须通过Tuplestore进行（而不是调用
 * 执行器）。
 */
void PersistHoldablePortal(Portal fc_portal)
{
	QueryDesc  *fc_queryDesc = fc_portal->queryDesc;
	Portal		fc_saveActivePortal;
	ResourceOwner fc_saveResourceOwner;
	MemoryContext fc_savePortalContext;
	MemoryContext fc_oldcxt;

	/*
	 * 如果我们正在保留一个可保持的门户，最好是在最初创建它的
	 * 事务内。
	 */
	Assert(fc_portal->createSubid != InvalidSubTransactionId);
	Assert(fc_queryDesc != NULL);

	/*
	 * 调用者必须已经创建了tuplestore……但不是快照。
	 */
	Assert(fc_portal->holdContext != NULL);
	Assert(fc_portal->holdStore != NULL);
	Assert(fc_portal->holdSnapshot == NULL);

	/*
	 * 在关闭执行器之前，我们必须将tupdesc复制到
	 * 长期内存中，因为它是在执行器内存中创建的。
	 */
	fc_oldcxt = MemoryContextSwitchTo(fc_portal->holdContext);

	fc_portal->tupDesc = CreateTupleDescCopy(fc_portal->tupDesc);

	MemoryContextSwitchTo(fc_oldcxt);

	/*
	 * 检查门户使用不当，并标记门户为活动。
	 */
	MarkPortalActive(fc_portal);

	/*
	 * 设置全局门户上下文指针。
	 */
	fc_saveActivePortal = ActivePortal;
	fc_saveResourceOwner = CurrentResourceOwner;
	fc_savePortalContext = PortalContext;
	PG_TRY();
	{
		ScanDirection fc_direction = ForwardScanDirection;

		ActivePortal = fc_portal;
		if (fc_portal->resowner)
			CurrentResourceOwner = fc_portal->resowner;
		PortalContext = fc_portal->portalContext;

		MemoryContextSwitchTo(PortalContext);

		PushActiveSnapshot(fc_queryDesc->snapshot);

		/*
		 * 如果门户被标记为可滚动，我们需要将整个
		 * 结果集存储在tuplestore中，以便后续的向后FETCH操作
		 * 可以处理。否则，只存储尚未获取的行。
		 * （后者不仅更高效，而且避免了当查询的输出不稳定时
		 * 的语义问题。）
		 *
		 * 在不可滚动的情况下，tuplestore中的元组索引将与
		 * 游标的名义位置（portalPos）不匹配。目前这
		 * 不会造成困难，因为我们只通过
		 * 相对位置在tuplestore中导航，除了下面的
		 * tuplestore_skiptuples调用和DoPortalRewind中的
		 * tuplestore_rescan调用，这两者在无滚动游标的情况下都是
		 * 被禁用的。但有一天，我们可能需要显式跟踪
		 * holdStore与游标的名义位置之间的偏移量。
		 */
		if (fc_portal->cursorOptions & CURSOR_OPT_SCROLL)
		{
			ExecutorRewind(fc_queryDesc);
		}
		else
		{
			/*
			 * 如果我们已经到达查询结束，设置方向为
			 * NoMovement，以避免尝试提取任何元组。（这个检查
			 * 存在是因为并非所有计划节点类型在已经返回NULL一次的情况下
			 * 都能稳健地再次被调用。）不过，我们仍然会设置一个空的
			 * tuplestore，以避免在后面出现特殊情况。
			 */
			if (fc_portal->atEnd)
				fc_direction = NoMovementScanDirection;
		}

		/*
		 * 将输出目标更改为tuplestore。注意，我们告诉
		 * tuplestore接收器对通过它传递的所有数据进行去烘焙；
		 * 这使得不保留与数据关联的快照变得安全。
		 */
		fc_queryDesc->dest = CreateDestReceiver(DestTuplestore);
		SetTuplestoreDestReceiverParams(fc_queryDesc->dest,
										fc_portal->holdStore,
										fc_portal->holdContext,
										true,
										NULL,
										NULL);

		/* 将结果集提取到tuplestore中 */
		ExecutorRun(fc_queryDesc, fc_direction, 0L, false);

		fc_queryDesc->dest->rDestroy(fc_queryDesc->dest);
		fc_queryDesc->dest = NULL;

		/*
		 * 现在关闭内部执行器。
		 */
		fc_portal->queryDesc = NULL;	/* 防止双重关闭 */
		ExecutorFinish(fc_queryDesc);
		ExecutorEnd(fc_queryDesc);
		FreeQueryDesc(fc_queryDesc);

		/*
		 * 设置结果集中的位置。
		 */
		MemoryContextSwitchTo(fc_portal->holdContext);

		if (fc_portal->atEnd)
		{
			/*
			 * 只是强制将tuplestore向前推进到末尾。这里的跳过请求大小是任意的。
			 */
			while (tuplestore_skiptuples(fc_portal->holdStore, 1000000, true))
				 /* 继续 */ ;
		}
		else
		{
			tuplestore_rescan(fc_portal->holdStore);

			/*
			 * 在无滚动的情况下，tuplestore的开始恰好是我们想要的位置，因此不需要重新定位。
			 */
			if (fc_portal->cursorOptions & CURSOR_OPT_SCROLL)
			{
				if (!tuplestore_skiptuples(fc_portal->holdStore,
										   fc_portal->portalPos,
										   true))
					elog(ERROR, "unexpected end of tuple stream");
			}
		}
	}
	PG_CATCH();
	{
		/* 执行门户时发生未捕获的错误：将其标记为已死 */
		MarkPortalFailed(fc_portal);

		/* 恢复全局变量并传播错误 */
		ActivePortal = fc_saveActivePortal;
		CurrentResourceOwner = fc_saveResourceOwner;
		PortalContext = fc_savePortalContext;

		PG_RE_THROW();
	}
	PG_END_TRY();

	MemoryContextSwitchTo(fc_oldcxt);

	/* 将门户标记为不活动 */
	fc_portal->status = PORTAL_READY;

	ActivePortal = fc_saveActivePortal;
	CurrentResourceOwner = fc_saveResourceOwner;
	PortalContext = fc_savePortalContext;

	PopActiveSnapshot();

	/*
	 * 现在我们可以释放门户上下文的任何附属内存；我们将不再使用它。执行器已经放弃了其上下文，但这将清理通过PortalContext附加到门户上下文的任何内容。
	 */
	MemoryContextDeleteChildren(fc_portal->portalContext);
}
